package fen

import (
	b "code.google.com/p/gochess/board"
	pos "code.google.com/p/gochess/position"
	"errors"
	"fmt"
	"strconv"
	"strings"
)

// todo: the error messages are hard to grasp. this is because the error messages are just passed through
func Parse(notation string) (*pos.Position, error) {

	fields := strings.Split(strings.TrimSpace(notation), " ")
	if len(fields) != 6 {
		return nil, errors.New(fmt.Sprintf("FEN must contain 6 fields, found %d", len(fields)))
	}

	activeColor, err := ParseActiveColor(fields[1])
	if err != nil {
		return nil, err
	}

	castlingWhite, castlingBlack, err := ParseCastlingAvailability(fields[2])
	if err != nil {
		return nil, err
	}

	epTarget, err := ParseEnPassantTarget(fields[3])
	if err != nil {
		return nil, err
	}

	halfmoveClock, err := parseHalfmoveClock(fields[4])
	if err != nil {
		return nil, err
	}

	// todo: implement fullmove counter in Position
	fullmoves, err := parseFullmoveNumber(fields[5])
	if err != nil {
		return nil, err
	}
	_ = fullmoves

	pieces, err := ParsePieces(fields[0])
	if err != nil {
		return nil, err
	}
	position := pos.NewPosition(activeColor, epTarget,
		castlingWhite, castlingBlack, halfmoveClock)
	for i := range pieces {
		position.Set(pieces[i].Piece, pieces[i].Location)
	}
	return position, err
}

func MustParse(notation string) *pos.Position {
	p, err := Parse(notation)
	if err != nil {
		panic(`fen: Parse(` + notation + `): ` + err.Error())
	}
	return p
}

type PlacedPiece struct {
	Location b.Square
	Piece    pos.Piece
}

var notation2Piece = map[string]pos.Piece{
	"K": pos.WhiteKing,
	"Q": pos.WhiteQueen,
	"R": pos.WhiteRook,
	"B": pos.WhiteBishop,
	"N": pos.WhiteKnight,
	"P": pos.WhitePawn,
	"k": pos.BlackKing,
	"q": pos.BlackQueen,
	"r": pos.BlackRook,
	"b": pos.BlackBishop,
	"n": pos.BlackKnight,
	"p": pos.BlackPawn,
}

// ParsePieces() parses the given fen notation and returns a map containing
// the pieces and their locations.
func ParsePieces(notation string) ([]PlacedPiece, error) {

	var pieces []PlacedPiece
	ranksFEN := strings.Split(notation, "/")
	if len(ranksFEN) != int(b.RowsBoard) {
		return nil, errors.New(fmt.Sprintf("fen: Piece placement field must contain 8 ranks, found %d", len(ranksFEN)))
	}
	for rank, rankFEN := range ranksFEN {
		if len(rankFEN) == 0 {
			return nil, errors.New(fmt.Sprintf("fen: Rank number %v contains no char! Needed at least one.", b.Rank(rank)))
		}
		file := b.A - 1
		for j := 1; j <= len(rankFEN); j++ {
			if file >= b.H {
				return nil, errors.New(fmt.Sprintf("fen: Rank number %v contains more than 8 files!", b.Rank(rank)))
			}
			char := rankFEN[j-1 : j]
			if piece, ok := notation2Piece[char]; ok {
				// a piece
				file++
				location := b.GetSquare(b.File(file), b.Rank8-b.Rank(rank))
				if !b.IsValid(location) {
					// this is not supposed to happen
					return nil, errors.New(fmt.Sprintf("fen: Location %d is not valid!", location))
				}
				pieces = append(pieces, PlacedPiece{location, piece})
			} else {
				// no piece, so hopefully a number of empty files
				if emptyFiles, err := strconv.Atoi(char); err == nil {
					file += b.File(emptyFiles)
				} else {
					return nil, errors.New(fmt.Sprintf("rank number %v contains invalid char '%s'!",
						b.Rank(rank), char))
				}
			}
			if file > b.H+1 {
				return nil, errors.New(fmt.Sprintf("Rank number %v contains more than 8 files!", b.Rank(rank)))
			}
		}
		if file != b.H {
			return nil, errors.New(fmt.Sprintf("fen: Rank number %v contains wrong number (%d) of files!", b.Rank(rank), file))
		}
	}
	return pieces, nil
}

var notation2Color = map[string]pos.Color{
	"w": pos.White,
	"b": pos.Black,
}

// second field: active color
func ParseActiveColor(notation string) (pos.Color, error) {
	if len(notation) != 1 {
		return pos.NoColor, errors.New(fmt.Sprintf("Second field must have only one char, found %v", len(notation)))
	}
	if _, ok := notation2Color[notation]; !ok {
		return pos.NoColor, errors.New(fmt.Sprintf("Second field contains invalid char %v.", notation))
	}
	return notation2Color[notation], nil
}

var notation2Castling = map[string]pos.Castling{
	"K": pos.Castlings[pos.White][pos.CastlingShort],
	"Q": pos.Castlings[pos.White][pos.CastlingLong],
	"k": pos.Castlings[pos.Black][pos.CastlingShort],
	"q": pos.Castlings[pos.Black][pos.CastlingLong],
}

// third field: castling availability
func ParseCastlingAvailability(notation string) (white, black pos.CastlingOption, err error) {

	var castlingOptions [pos.BiggestColor + 1]pos.CastlingOption
	if len(notation) > 4 {
		return pos.NoCastling, pos.NoCastling,
			errors.New("castling availability field contains more than 4 char")
	}
	if notation == "-" {
		return pos.NoCastling, pos.NoCastling, nil
	}

	var castling pos.Castling
	var ok bool
	for i := range notation {
		char := notation[i : i+1]
		if castling, ok = notation2Castling[char]; !ok {
			return pos.NoCastling, pos.NoCastling,
				errors.New(fmt.Sprintf("castling availability field contains invalid char '%v'",
					char))
		}
		castlingOptions[castling.Color] |= castling.Option
	}
	return castlingOptions[pos.White], castlingOptions[pos.Black], nil
}

var notation2File = map[string]b.File{
	"a": b.A,
	"b": b.B,
	"c": b.C,
	"d": b.D,
	"e": b.E,
	"f": b.F,
	"g": b.G,
	"h": b.H,
}

var notation2epRank = map[string]b.Rank{
	"3": b.Rank3,
	"6": b.Rank6,
}

// forth field: en passant target square
// todo: implements its own parsefile()
func ParseEnPassantTarget(notation string) (b.Square, error) {

	if notation == "-" {
		return b.NoSquare, nil
	}
	if len(notation) != 2 {
		return b.NoSquare, errors.New("fen: '" + notation + "' is an invalid square notation")
	}
	file, ok := notation2File[notation[:1]]
	if !ok {
		return b.NoSquare, errors.New("fen: '" + notation + "' contains invalid file character")
	}
	rank, ok := notation2epRank[notation[1:]]
	if !ok {
		return b.NoSquare, errors.New("fen: '" + notation + "' contains invalid e.p. rank character")
	}
	return b.GetSquare(file, rank), nil
}

// fifth field: half move clock
func parseHalfmoveClock(notation string) (int32, error) {
	halfmoveClock, err := strconv.Atoi(notation)
	if err != nil {
		return int32(halfmoveClock), err
	}
	if halfmoveClock < 0 {
		return 0, errors.New("halfmove clock must be >= 0")
	}
	return int32(halfmoveClock), err
}

// sixth field: fullmove number
// todo: implement this
func parseFullmoveNumber(notation string) (int, error) {
	var fullmoves int
	return fullmoves, nil
}
